/**
   Copyright 2010 Carlo Alberto Degli Atti (lord kada)

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/

package it.concept.pin;

import it.concept.pin.annotations.CollectionMap;
import it.concept.pin.annotations.Map;
import it.concept.pin.annotations.MapItem;
import it.concept.pin.annotations.Model;
import it.concept.pin.annotations.ToModelAdapter;
import it.concept.pin.annotations.ToPojoAdapter;
import it.concept.pin.annotations.Transport;
import it.concept.pin.exceptions.DataModelFieldDoesNotExist;
import it.concept.pin.exceptions.DataModelMethodDoesNotExist;
import it.concept.pin.exceptions.PinException;
import it.concept.pin.exceptions.SetterMethodIsNotVoid;
import it.concept.pin.exceptions.WrongNumberOfParametersInSetterMethod;
import it.concept.pin.proxy.ProxyWrapper;
import it.concept.pin.transport.ToModelTransportHandler;
import it.concept.pin.transport.ToPojoTransportHandler;

import java.lang.annotation.AnnotationFormatError;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class PinManager {

	private static final String GETTER_METHOD_PREFIX = "get";
	
	private static final String SETTER_METHOD_PREFIX = "set";

	/**
	 * This method converts a model into a pojo
	 * @param targetPojoClass
	 * @param model
	 * @return
	 * @throws PinException
	 */
	public static Object toPojo( Class<?> targetPojoClass, Object model ) throws PinException {
		
		return toPojo( targetPojoClass, model, null );
	}

	/**
	 * This method converts a model into a pojo passing the context to any custom TransportHandler
	 * @param targetPojoClass
	 * @param model
	 * @param context
	 * @return
	 * @throws PinException
	 */
	@SuppressWarnings("unchecked")
	public static Object toPojo( Class<?> targetPojoClass, Object model, Context context ) throws PinException {
		
		Object instancedPojo = null;
		
		if ( model != null ) {
				
			try {
				
				/*
				 * POJO instantiation
				 */
				if ( targetPojoClass.getAnnotation( Model.class ) == null )
					
					throw new AnnotationFormatError( targetPojoClass.getName() + " is not @Pojo annotated" );
				
				Map mapAnnotation = targetPojoClass.getAnnotation( Map.class );
				
				if ( mapAnnotation != null ) {
				
					MapItem[] value = mapAnnotation.value();
					
					for ( int i = 0; i < value.length; i++ ) {
						
						if ( value[ i ].model().getName().compareTo( model.getClass().getName() ) == 0 ) {
							
							instancedPojo = newInstance( value[ i ].pojo() );
							break;
							
						}
						
					}
					
				}

				if ( instancedPojo == null )
					instancedPojo = newInstance( targetPojoClass );
				
				/*
				 * POJO fields
				 */
				// Let's process the class members annotated with @Transport
				for ( Field pojoField : getFields( instancedPojo.getClass() ) ) {
					
					pojoField.setAccessible( true );
					
					Transport pojoFieldAnnotation = pojoField.getAnnotation( Transport.class );
					
					if ( pojoFieldAnnotation != null ) {
						
						Field modelField = getField( pojoField.getName(), model.getClass() );

						if ( modelField != null ) {

							modelField.setAccessible( true );
							
							Object valueToSet = modelField.get( model );
							
							if ( valueToSet instanceof Collection<?> ) {
								
								CollectionMap annotation = pojoField.getAnnotation( CollectionMap.class );
								
								Collection<Object> newInstance = (Collection<Object>) ( ( annotation != null && annotation.pojo() != null && Collection.class.isAssignableFrom( annotation.pojo() ) ) ? PinManager.newInstance( annotation.pojo() ) :  newInstance( valueToSet.getClass() ) );
								
								if ( ! pojoFieldAnnotation.value().equals( Void.class ) ) {
									
									for ( Object item : ( Collection<Object> ) valueToSet )
										
										newInstance.add( toPojo( pojoFieldAnnotation.value(), item, context ) );
									
									valueToSet = newInstance;
									
								}
								
							}
							
							else
								
								if ( pojoField.getType().getAnnotation( Model.class ) != null )
								
									valueToSet = toPojo( pojoField.getType(), valueToSet, context );
								
							pojoField.set( instancedPojo, valueToSet );

						}
						
						else 
							
							throw new DataModelFieldDoesNotExist( model.getClass().getName(), pojoField.getName() );
						
					}
					
				}
				
				/*
				 * POJO setter methods
				 */
				// Let's process the setter methods that are annotated with @Transport
				// Note that a setter method: 
				// a) returns void b) expects one parameter c) name begins with 'set'
				for ( Method setterMethod : getMethods( instancedPojo.getClass() ) ) {
				
					Transport methodAnnotation = setterMethod.getAnnotation( Transport.class );
					
					if ( methodAnnotation != null ) {
						
						if ( setterMethod.getReturnType().equals( void.class ) ) {
						
							setterMethod.setAccessible( true );
						
							Class<?>[] parameterTypes = setterMethod.getParameterTypes();
						
							if ( parameterTypes.length != 1 )
								
								throw new WrongNumberOfParametersInSetterMethod( setterMethod.getName() );
							
							String modelGetterMethodName = setterMethod.getName().replaceAll( SETTER_METHOD_PREFIX, GETTER_METHOD_PREFIX );
							
							Method modelGetterMethod = getMethod( modelGetterMethodName, new Class<?>[]{}, model.getClass() );
	
							if ( modelGetterMethod != null ) {
								
								modelGetterMethod.setAccessible( true );
								
								Object valueToSet = modelGetterMethod.invoke( model, new Object[] { } );
								
								if ( valueToSet instanceof Collection<?> ) {
									
									CollectionMap annotation = setterMethod.getAnnotation( CollectionMap.class );
									
									Collection<Object> newInstance = ( Collection<Object> ) ( ( annotation != null && annotation.pojo() != null && Collection.class.isAssignableFrom( annotation.pojo() ) ) ? PinManager.newInstance( annotation.pojo() ) : newInstance( valueToSet.getClass() ) );
									
									if ( ! methodAnnotation.value().equals( Void.class ) ) {
										
										for ( Object item : ( Collection<Object> ) valueToSet )
											
											newInstance.add( toPojo( methodAnnotation.value(), item, context ) );
										
										valueToSet = newInstance;
										
									}
									
								}
								
								else 
									
									if ( parameterTypes[0].getAnnotation( Model.class ) != null )

										valueToSet = toPojo( parameterTypes[0], valueToSet, context );
									
								setterMethod.invoke( instancedPojo, new Object[] { valueToSet } );

							}
							
							else
								
								throw new DataModelMethodDoesNotExist( model.getClass().getName(), modelGetterMethodName );
								
						}
						
						else
							
							throw new SetterMethodIsNotVoid( setterMethod.getName() );
						
					}
					
				}
				
				/*
				 * TransportAdapter
				 */
				ToPojoAdapter transportAnnotation = instancedPojo.getClass().getAnnotation( ToPojoAdapter.class );
				if ( transportAnnotation != null ) {
					
					Constructor<?> constructor = transportAnnotation.value().getConstructor( new Class<?>[]{ Context.class } );
					
					ToPojoTransportHandler<Object, Object> transportAdapter = ( ToPojoTransportHandler<Object, Object> ) constructor.newInstance( context );
					
					instancedPojo = transportAdapter.toPojo( instancedPojo, model );
					
				}
				
			}
			catch ( Exception e ) {
				
				throw new PinException( e );
				
			}
		
		}
			
		return instancedPojo;
		
	}

	/**
	 * This method converts a pojo into a model
	 * @param pojo
	 * @return
	 * @throws PinException
	 */
	public static Object toModel( Object pojo ) throws PinException {
		
		return toModel( pojo , null );
		
	}
	
	/**
	 * This method converts a pojo into a model passing the context to any custom TransportHandler
	 * @param pojo
	 * @param context
	 * @return
	 * @throws PinException
	 */
	public static Object toModel( Object pojo, Context context ) throws PinException {
	
		Object modelObject = null;
		
		if ( pojo != null ) {
		
			Class<?> pojoClass = pojo.getClass();
			Model annotation = pojoClass.getAnnotation( Model.class );
			
			if ( annotation == null )
				
				throw new AnnotationFormatError( pojoClass.getName() + " is not @Pojo annotated" );
			
			modelObject = ( annotation.value().isInterface() ) ? toInterface( pojo, context ) : toImplementation( pojo, context );
			
		}
		
		return modelObject;
		
	}
	
	@SuppressWarnings("unchecked")
	private static Object toImplementation( Object pojo, Context context ) throws PinException {
		
		Object implementationObject = null;
		
		Class<?> pojoClass = pojo.getClass();
		
		try {
			
			Map mapAnnotation = pojoClass.getAnnotation( Map.class );
			
			if ( mapAnnotation != null ) {
			
				MapItem[] value = mapAnnotation.value();
				
				for ( int i = 0; i < value.length; i++ ) {
					
					if ( value[ i ].pojo().getClass().equals( pojo.getClass() ) ) {
						
						implementationObject = newInstance( value[ i ].model() );
						break;
						
					}
					
				}
				
			}

			if ( implementationObject == null )
				implementationObject = newInstance( pojoClass.getAnnotation( Model.class ).value() );
			
			Class<?> implementationClass = implementationObject.getClass();
			
			// Let's process the class members annotated with @Transport
			for ( Field pojoField : getFields( pojoClass ) ) {
				
				pojoField.setAccessible( true );
				
				Transport pojoFieldAnnotation = pojoField.getAnnotation( Transport.class );
				
				if ( pojoFieldAnnotation != null ) {
					
					Field destinationField = getField( pojoField.getName(), implementationClass );

					if ( destinationField != null ) {

						destinationField.setAccessible( true );
						
						Object valueToSet = pojoField.get( pojo );
						
						if ( valueToSet instanceof Collection<?> ) {
							
							CollectionMap annotation = pojoField.getAnnotation( CollectionMap.class );
							
							Collection<Object> newInstance = (Collection<Object>) ( ( annotation != null && annotation.model() != null && Collection.class.isAssignableFrom( annotation.model() ) ) ? PinManager.newInstance( annotation.model() ) :  newInstance( valueToSet.getClass() ) );
								
							for ( Object item : ( Collection<Object> ) valueToSet )
									
								newInstance.add( ( item.getClass().getAnnotation( Model.class ) != null )?toModel( item, context ) : item );
								
							valueToSet = newInstance;
							
						}
						
						else
							
							if ( pojoField.getType().getAnnotation( Model.class ) != null )
							
								valueToSet = toModel( valueToSet, context );

						destinationField.set( implementationObject, valueToSet );

					}
					
					else 
						
						throw new DataModelFieldDoesNotExist( implementationClass.getClass().getName(), pojoField.getName() );
					
				}
				
			}
			
			// Let's process the setter methods that are annotated with @Transport
			// Note that a setter method: 
			// a) returns void b) expects one parameter c) name begins with 'set'
			for ( Method pojoSetterMethod : getMethods( pojoClass ) ) {
				
				Transport methodAnnotation = pojoSetterMethod.getAnnotation( Transport.class );
				
				if ( methodAnnotation != null ) {
					
					if ( pojoSetterMethod.getReturnType().equals( void.class ) ) {
					
						pojoSetterMethod.setAccessible( true );
					
						Class<?>[] parameterTypes = pojoSetterMethod.getParameterTypes();
					
						if ( parameterTypes.length != 1 )
							
							throw new WrongNumberOfParametersInSetterMethod( pojoSetterMethod.getName() );
						
						String modelGetterMethodName = pojoSetterMethod.getName().replaceAll( SETTER_METHOD_PREFIX, GETTER_METHOD_PREFIX );
						
						Method modelGetterMethod = getMethod( modelGetterMethodName, new Class<?>[]{}, pojoClass );

						if ( modelGetterMethod != null ) {
							
							modelGetterMethod.setAccessible( true );
							
							Object valueToSet = modelGetterMethod.invoke( pojo, new Object[] { } );
							
							if ( valueToSet instanceof Collection<?> ) {
								
								CollectionMap annotation = pojoSetterMethod.getAnnotation( CollectionMap.class );
								
								Collection<Object> newInstance = (Collection<Object>) ( ( annotation != null && annotation.model() != null && Collection.class.isAssignableFrom( annotation.model() ) ) ? PinManager.newInstance( annotation.model() ) :  newInstance( valueToSet.getClass() ) );

								for ( Object item : ( Collection<Object> ) valueToSet )
									
									newInstance.add( ( item.getClass().getAnnotation( Model.class ) != null ) ? toModel( item, context ) : item );
								
								valueToSet = newInstance;
									
							}
							
							else 
								
								if ( parameterTypes[0].getAnnotation( Model.class ) != null )

									valueToSet = toModel( valueToSet, context );
							
							// Let's invoke the setter method on the implementation class
							Method method = getMethod( pojoSetterMethod.getName(), new Class<?>[]{ ( valueToSet != null ) ? valueToSet.getClass() : null }, implementationClass );
							method.setAccessible( true );
							method.invoke( implementationObject, new Object[] { valueToSet } );

						}
						
						else
							
							throw new DataModelMethodDoesNotExist( implementationObject.getClass().getName(), modelGetterMethodName );
							
					}
					
					else
						
						throw new SetterMethodIsNotVoid( pojoSetterMethod.getName() );
					
				}
				
			}

			/*
			 * TransportAdapter
			 */
			ToModelAdapter transportAnnotation = pojo.getClass().getAnnotation( ToModelAdapter.class );
			if ( transportAnnotation != null ) {
				
				Constructor<?> constructor = transportAnnotation.value().getConstructor( new Class<?>[]{ Context.class } );
				
				ToModelTransportHandler<Object, Object> transportAdapter = ( ToModelTransportHandler<Object, Object> ) constructor.newInstance( context );
				
				implementationObject = transportAdapter.toModel( pojo, implementationObject );
				
			}
			
		}
		catch ( Exception e ) {
			
			e.printStackTrace();
			throw new PinException( e );
			
		}
		
		return implementationObject;
		
	}

	private static Object toInterface( Object pojo, Context context ) throws PinException {

		try {
			
			return ProxyWrapper.createProxy( pojo.getClass().getAnnotation( Model.class ).value(), pojo, context );
			
		} catch ( Exception e ) {
			
			throw new PinException( e );
			
		}
		
	}
	
	private static List<Field> getFields( Class<?> type ) {
		
		return _getFields( new ArrayList<Field>(), type );
		
	}
	
	private static List<Field> _getFields( List<Field> fields, Class<?> type ) {
		
		if ( type.getSuperclass() != null ) 
	    	
	        _getFields( fields, type.getSuperclass() );

    	for ( Field field: type.getDeclaredFields() )

	    	fields.add(field);
	
	    return fields;
	
	}
	
	private static Field getField( String fieldName, Class<?> type ) throws NoSuchFieldException {
		
		try {
			
			return type.getDeclaredField( fieldName );
		
		} catch( NoSuchFieldException e ) {

			Class<?> superclass = type.getSuperclass();
			
			if ( superclass != null) 
				
				return getField( fieldName, superclass );
			
			throw e;
			
		}
	
	}

	private static List<Method> getMethods( Class<?> type ) {
		
		return _getMethods( new ArrayList<Method>(), type );
		
	}
	
	private static List<Method> _getMethods( List<Method> methods, Class<?> type ) {
		
		if ( type.getSuperclass() != null ) 

			_getMethods( methods, type.getSuperclass() );
		
	    for ( Method method: type.getDeclaredMethods() )
	        
	    	if ( method.getName().startsWith( "set") )
	    			
	    			methods.add( method );

	    return methods;
	    
	}
	
	private static Method getMethod( String methodName, Class<?>[] args, Class<?> type ) throws Exception {
		
		Method[] declaredMethods = type.getDeclaredMethods();
		
		for ( int index = 0; index < declaredMethods.length; index++ ) {
			
			Method method = declaredMethods[ index ];
			
			if ( method.getName().compareTo( methodName ) == 0 ) {
			
				Class<?>[] parameterTypes = method.getParameterTypes();
				
				if ( parameterTypes.length == args.length ) {
				
					boolean compatibleFlag = true;
					
					for ( int jindex=0; jindex < parameterTypes.length; jindex++ ) {
					
						Class<?> parameterType = parameterTypes[ jindex ];
						
						if ( args[ jindex ] != null && !( parameterType.isPrimitive() && args[ jindex ].getField( "TYPE" ).get( null ).equals( parameterType ) ) && !parameterType.isAssignableFrom( args[ jindex ] ) ) {
							
							compatibleFlag = false;
							break;
						
						}
					
					}
					
					if ( compatibleFlag )
						return method;
				
				}
				
			}
			
		}
		
		Class<?> superclass = type.getSuperclass();
		
		if ( superclass != null) 
			
			return getMethod( methodName, args, superclass );
		
		throw new NoSuchMethodException( methodName + " in type " + type.getName() );
			
	}
	
	public static Object newInstance( Class<?> className ) throws SecurityException, NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException, InvocationTargetException {
		
		Object newInstance = null;
		
		if ( className != null ) {
		
			Constructor<?> constructor = className.getDeclaredConstructor( new Class<?>[]{} );
			constructor.setAccessible( true );
			
			return constructor.newInstance( new Object[]{} );
			
		}
		
		return newInstance;
		
	
	}
	
}